iT邦幫忙

2026 iThome 鐵人賽

DAY 6
0
AI 自動化

測試的前提正在改變:AI 時代下 QA 的思維轉換系列 第 6

[Day6] 善於 hook 做防線!AI 模型越來越強!但我需要一個可控制且安全的 AI!

  • 分享至 

  • xImage
  •  

Hooks 是在 Claude Code 生命週期的特定時間點(工具呼叫前/後、對話開始/結束等)自動觸發指定的腳本。
它跟「CLAUDE.md」最大的差別在於:

  • CLAUDE.md 的規則是「提醒」,Claude 容易因為對話太長而忘記
  • Hooks 是「強制」,不管 Claude 記不記得,腳本一定會觸發,只就能有效的提高我們控制率

重點摘要

  • hook 三條防線機制
  • 破壞性指令:不是禁止,是換一個人按 Enter
  • 憑證:不是不能查,是只回報有沒有、不回報是什麼
  • 對外寫入:只走封裝好的路,沒有路就停下來問
  • 規則要誠實標示邊界——知道哪裡有洞,跟以為沒洞是兩回事
  • 會誤擋的規則比沒規則更糟,因為它會被關掉

AI 最壞情況會怎樣?

最害怕 AI 失控做出很多非預期的行為

我在設計這幾條線的時候,只問一個問題:

如果這一步 AI 判斷錯了,我還救得回來嗎?

直接切成兩堆:

救得回來 救不回來
改壞一支腳本 → git 還原 rm -rf 刪掉整個目錄
寫錯一份文件 → 重寫 金鑰進了對話紀錄 → 資安問題,只能整批換掉
跑錯一次測試 → 重跑 改其他人的工作單的狀態或者亂留言
產出一份爛報告 → 重產 git --fouse 強制推送分支,直接覆蓋別人的程式碼

hook 三條防線

具體落成三個規則:

規則
破壞性指令 AI 不自己執行,改成把指令列給我,我自己貼
憑證 不進 context。連讀都不要讀
對外寫入 只能走已經封裝好的 function,沒有就停下來問

第一條:破壞性指令,AI 列給我,我自己執行

這條的重點不是「禁止」,是換一個執行者

因為那些指令本身是必要的。
重構的時候真的要砍掉一批檔案,rebase 完真的要強制推送。

問題不在指令,在「誰按下 Enter」。

所以做法是:AI 想跑這類指令的時候,攔下來,讓它改成:

這一步需要執行 <完整指令>,它會做 <什麼事>,請你確認路徑後自己執行。

我把決定權拿回來,同時保留了它幫我把指令組好的價值。

範本:攔截 hook

Claude Code 有 PreToolUse hook,會在指令執行前先跑一段你自己的程式,回傳 deny 就擋下來。

先在 .claude/settings.json 掛上:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "python3 \"$CLAUDE_PROJECT_DIR/.claude/hooks/block_destructive.py\""
          }
        ]
      }
    ]
  }
}

然後 block_destructive.py 的大致骨架:

#!/usr/bin/env python3
"""PreToolUse hook:攔截破壞性指令,改由使用者手動執行。"""
import json
import re
import sys

DANGER = [
    (r"\brm\s+(-\w*\s+)*-\w*[rf]",          "遞迴/強制刪除"),
    (r"\brm\s+.*\*",                         "萬用字元刪除"),
    (r"\bgit\s+push\s+.*(-f\b|--force)",     "強制 push"),
    (r"\bgit\s+reset\s+--hard",              "硬重置"),
    (r"\bgit\s+clean\s+-\w*[fd]",            "清除未追蹤檔"),
    (r"\bdd\s+",                             "磁碟寫入"),
]


def block(cmd, desc):
    reason = (
        f"⛔ 已被安全 hook 攔截:{desc}\n"
        f"指令:{cmd}\n\n"
        "規範:不要自己執行,也不要重試。"
        "請把此指令完整列給使用者、說明它會做什麼,由使用者自行執行。"
    )
    print(json.dumps({
        "hookSpecificOutput": {
            "hookEventName": "PreToolUse",
            "permissionDecision": "deny",
            "permissionDecisionReason": reason,
        }
    }))
    sys.exit(0)


def main():
    data = json.load(sys.stdin)
    if data.get("tool_name") != "Bash":
        sys.exit(0)
    cmd = data.get("tool_input", {}).get("command", "")

    for pattern, desc in DANGER:
        if re.search(pattern, cmd):
            block(cmd, desc)
    sys.exit(0)


if __name__ == "__main__":
    main()

重點在 block() 裡那句「不要重試」。

沒寫這句的話,AI 被擋下之後會很努力地換各種寫法再試一次,然後你就會看到它連續撞牆五次。

要明確告訴它:這不是失敗,這是規則,改成列給使用者就對了!


第二條:機敏憑證不進 context

上面擋的是「動作」,這條擋的是「看見」。

因為金鑰只要進了對話紀錄,它就可能被帶進:

  • 你貼給同事的截圖
  • 產出的報告
  • 存下來的 log
  • 送出去的 issue 內容

而且你不會發現,因為它只是混在一大段內容裡的一串字。

範本:擋掉憑證檔的讀取

{
  "permissions": {
    "deny": [
      "Read(.env)",
      "Read(**/.env)",
      "Bash(cat *.env*)",
      "Bash(cat **/.env*)",
      "Bash(grep * .env*)",
      "Bash(head * .env*)",
      "Bash(tail * .env*)",
      "Bash(less .env*)",
      "Bash(more .env*)"
    ]
  }
}

AI 排查的時候真的需要知道「有沒有設」

這是實際會遇到的:某個功能跑不起來,要確認是不是少了某把 key。

擋掉讀取之後,這件事怎麼做?

答案是給一個只回報存在與否、不印值的檢查指令。

# config_check.py —— 只說有沒有,不說是什麼
REQUIRED = ["API_TOKEN", "SERVICE_URL", "ACCOUNT", "PASSWORD"]

def check():
    for key in REQUIRED:
        exists = bool(os.getenv(key))
        print(f"{'✅' if exists else '❌'} {key}")

輸出長這樣:

✅ API_TOKEN
❌ SERVICE_URL
✅ ACCOUNT
✅ PASSWORD

第三條:對外寫入只能走封裝

呼叫外部系統做「新增/修改/刪除」

  • Jira 開 Bug 票、改狀態
  • TestRail 留言、修改
  • Github 開 PR
  • ...等等

這些一旦送出去,就在別人眼前了
如果只是人為溝通一下,或許可以互相體諒,手動調整

但萬一對方流程中背後有相關自動化再執行,這時候誤觸發可能會導致背後流程都白白觸發了(甚至有需要話錢的話更恐怖)

身爲 QA 還是謹慎為上才是上上策XDD
身為QA,小心謹慎為上上策

我自己的規則是:

這類操作只能呼叫已經封裝好、審查過的 function。
沒有對應的 function,就停下來問我,不准自己組一個。

為什麼要這樣

因為如果放任 AI 自己拼 API 呼叫,它會每次都拼出不一樣的版本

  • 這次帶了必要參數,下次忘了
  • 這次有做前置檢查,下次直接送
  • 這次錯誤處理寫了,下次沒有

而封裝好的 function 是寫過一次、審過一次、防呆做過一次,之後每次都走同一條路。

這條規則有個漏洞,要補起來

規則寫在文件裡,但 AI 想繞過還是繞得過

所以這條要跟第一條的 hook 合併使用,把「未經封裝的對外寫入」也加進攔截清單:

NET_WRITE = [
    (r"\bcurl\b[^\n]*(-X\s*(POST|PUT|DELETE|PATCH)\b)", "未走封裝的對外寫入"),
    (r"\brequests\.(post|put|delete|patch)\s*\(",       "未走封裝的對外寫入"),
    (r"\b(shutil\.rmtree|os\.remove|os\.unlink)\b",     "程式碼刪檔"),
]

被擋到的時候,提示要換一種:

這個寫入操作尚未封裝。請停下來告訴使用者,詢問是否要新增到封裝層,不要自己組呼叫繞過

這樣「文件裡的規範」就變成「機器擋得住的規則」。


這三條線對 QA 的意義

寫到這裡可能會覺得這篇好像跟 QA 無關?

但它的核心還是跟 QA 有掛鈎的:

我們判斷風險的方式,從來就不是「這件事會不會出錯」,
而是「出錯的時候,代價、成本是什麼」。

這其實跟排查 Bug 優先序完全是同一套思路。

這篇做的事情,就是把那個提醒從「我每次"記得"問」變成「程式碼 "固定" 每次幫我擋」。

這也個流程相當關鍵!也會跟後續 AI E2E 文章會有非常大的相依


有基礎防線了,但終究是擋君子不擋小人

上面那些規則 擋不住所有繞法

隨便舉一個它就穿了:

python3 -c "print(open('.env').read())"

我沒有辦法窮舉所有繞過方式

所以這條規則的定位要說清楚:

它擋的是日常誤用,不是惡意繞過。
它降低「不小心把 token 帶進 context」的機率,但它不取代「不把敏感內容寫進輸出」這個行為規範。

我覺得誠實標示一個防護的邊界,比假裝它滴水不漏重要。

知道防線在哪裡有洞,跟以為自己沒有洞,是完全不同的安全狀態的。

參考文獻


不過這裡有個問題。

上面這些規則,有些是 hook 在守(AI 繞不過),有些只是寫在文件裡(全靠自律)。

而寫在文件裡的那些,我完全不知道它們到底有沒有被遵守。

明天就講怎麼我初步會怎麼區分相關規則,讓 AI 可以既成本低且能精準執行


上一篇
[Day5] 寫了一支很完整的 Skill,然後 AI 從來沒被叫出來過?所以理解使用情境真的很重要!
下一篇
[Day7] 問 AI「你剛剛做得對嗎」,它可能永遠都說對
系列文
測試的前提正在改變:AI 時代下 QA 的思維轉換8
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言